// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: yuxuan

#pragma once

#include <algorithm>
#include <cstdlib>
#include <string>
#include <vector>

#include "cc/coin2/exchange/base/order/order.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book.h"

namespace coin2::strategy::util {
using coin2::exchange::base::feed::FeedUpdate;
using coin2::exchange::base::symbology::IProductHolder;

template <typename T>
bool IsClose(T a, T b) {
  return std::abs(a - b) <= std::min(1.e-8, 1.e-6 * std::max(std::abs(a), std::abs(b)));
}

class TopBook {
 public:
  TopBook(int levels, int book_change_levels, double bp, int warmup_sec)
      : levels_(levels),
        book_change_levels_(book_change_levels),
        bid_levels_(0),
        ask_levels_(0),
        book_warmup_time_(static_cast<int64_t>(warmup_sec) * 1'000'000'000LL) {
    CHECK((levels > 0) ^ (bp > 0)) << levels << " " << bp;
    if (levels > 0) {
      bids_.assign(levels, {0, 0});
      asks_.assign(levels, {0, 0});
      bp_ = 1;
    } else {
      // allow up to 20 levels
      bids_.assign(20, {0, 0});
      asks_.assign(20, {0, 0});
      levels_ = 20;
      bp_ = bp * 1e-4;
    }
  }

  void UpdateProductInfo(const IProductHolder& holder) {
    if (holder.product().market() == MarketType::Spot) {
      contract_value_ = 1;
    } else {
      contract_value_ = holder.product_info().contract_value();
      is_inverse_ = holder.product_info().is_inverse();
    }
  }

  void UpdateTs(int64_t ts) { ts_ = ts; }

  bool UpdateFeed(const FeedUpdate& upd) {
    if (book_warmup_time_ == 0 ||
        (first_feed_ts_ > 0 && upd.timestamp() - first_feed_ts_ >= book_warmup_time_)) {
      ready_ = true;
      ts_ = upd.timestamp();
      exchange_ts_ = upd.ts().exchange_publish_timestamp();
      bool has_update = false;
      bid_levels_ = ask_levels_ = 0;
      if (upd.book().BidSize() > 0) {
        int i = 0;
        double stop_price = upd.book().Bid0()->price * (1 - bp_);
        for (auto iter = upd.book().BidBegin(); iter != upd.book().BidEnd(); ++iter) {
          auto price = iter->first;
          auto qty = iter->second;
          if (!IsClose(price, bids_[i].price) || !IsClose(qty, bids_[i].qty)) {
            if (i < book_change_levels_) {
              has_update = true;
            }
            bids_[i].price = price;
            bids_[i].qty = qty;
          }
          if (++i >= levels_ || price <= stop_price) {
            break;
          }
        }
        bid_levels_ = i;
      }
      if (upd.book().AskSize() > 0) {
        int i = 0;
        double stop_price = upd.book().Ask0()->price * (1 + bp_);
        for (auto iter = upd.book().AskBegin(); iter != upd.book().AskEnd(); ++iter) {
          auto price = iter->first;
          auto qty = iter->second;
          if (!IsClose(price, asks_[i].price) || !IsClose(qty, asks_[i].qty)) {
            if (i < book_change_levels_) {
              has_update = true;
            }
            asks_[i].price = price;
            asks_[i].qty = qty;
          }
          if (++i >= levels_ || price >= stop_price) {
            break;
          }
        }
        ask_levels_ = i;
      }
      return has_update && HasMid();
    } else if (first_feed_ts_ == 0) {
      first_feed_ts_ = upd.timestamp();
    }
    return false;
  }

  bool HasMid() const { return bid_levels_ > 0 && ask_levels_ > 0; }

  double Mid() const { return 0.5 * (GetBid(0).price + GetAsk(0).price); }

  const std::vector<PriceQty>& Bids() const { return bids_; }
  const std::vector<PriceQty>& Asks() const { return asks_; }

  const PriceQty& GetBid(int level) const { return bids_[level]; }
  const PriceQty& GetAsk(int level) const { return asks_[level]; }

  int BidLevels() const { return bid_levels_; }
  int AskLevels() const { return ask_levels_; }
  int Levels() const { return levels_; }

  double BidQtyUpToLevels(int levels) const {
    double qty = 0;
    for (int i = 0; i < levels && i < bid_levels_; ++i) {
      qty += bids_[i].qty;
    }
    return qty;
  }

  double AskQtyUpToLevels(int levels) const {
    double qty = 0;
    for (int i = 0; i < levels && i < ask_levels_; ++i) {
      qty += asks_[i].qty;
    }
    return qty;
  }

  double BidQtyUpToBp(double bp) const {
    double qty = 0;
    if (bid_levels_ > 0) {
      double stop_price = bids_[0].price * (1 + bp / 10000);
      for (int i = 0; i < bid_levels_; ++i) {
        if (bids_[i].price <= stop_price) {
          qty += bids_[i].qty;
        } else {
          break;
        }
      }
    }
    return qty;
  }

  double AskQtyUpToBp(double bp) const {
    double qty = 0;
    if (ask_levels_ > 0) {
      double stop_price = asks_[0].price * (1 + bp / 10000);
      for (int i = 0; i < ask_levels_; ++i) {
        if (asks_[i].price <= stop_price) {
          qty += asks_[i].qty;
        } else {
          break;
        }
      }
    }
    return qty;
  }

  bool AskPriceUpToNotional(double notional, double* price) const {
    CHECK(contract_value_ > 0) << "contract_value not initialized";
    if (is_inverse_) {
      for (int i = 0; i < ask_levels_; ++i) {
        notional -= asks_[i].qty * contract_value_;
        if (notional <= 0) {
          *price = asks_[i].price;
          return true;
        }
      }
    } else {
      for (int i = 0; i < ask_levels_; ++i) {
        notional -= asks_[i].qty * asks_[i].price * contract_value_;
        if (notional <= 0) {
          *price = asks_[i].price;
          return true;
        }
      }
    }
    if (ask_levels_ > 0) {
      *price = asks_[ask_levels_ - 1].price;
      return true;  // fall back to last
    }
    return false;
  }

  bool BidPriceUpToNotional(double notional, double* price) const {
    CHECK(contract_value_ > 0) << "contract_value not initialized";
    if (is_inverse_) {
      for (int i = 0; i < bid_levels_; ++i) {
        notional -= bids_[i].qty * contract_value_;
        if (notional <= 0) {
          *price = bids_[i].price;
          return true;
        }
      }
    } else {
      for (int i = 0; i < bid_levels_; ++i) {
        notional -= bids_[i].qty * bids_[i].price * contract_value_;
        if (notional <= 0) {
          *price = bids_[i].price;
          return true;
        }
      }
    }
    if (bid_levels_ >= 0) {
      *price = bids_[bid_levels_ - 1].price;
      return true;  // fall back to last
    }
    return false;
  }

  int64_t Ts() const { return ts_; }
  int64_t ExchangeTs() const { return exchange_ts_; }
  bool Ready() const { return ready_; }

 private:
  int levels_;
  double bp_;
  int book_change_levels_;
  int bid_levels_;
  int ask_levels_;
  std::vector<PriceQty> bids_;
  std::vector<PriceQty> asks_;
  double contract_value_{0};
  bool is_inverse_{false};
  int64_t ts_{0};
  int64_t exchange_ts_{0};
  int64_t first_feed_ts_{0};
  bool ready_{false};
  const int64_t book_warmup_time_;
};
}  // namespace coin2::strategy::util
